// SPDX-License-Identifier: GPL-2.0
/*
 * Lombo ir driver
 *
 * Copyright (C) 2016-2019, LomboTech Co.Ltd.
 * Author: lomboswer <lomboswer@lombotech.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 */
#include <linux/device.h>
#include <linux/err.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/module.h>
#include <linux/of_platform.h>
#include <linux/platform_device.h>
#include <linux/clk.h>
#include <media/rc-core.h>
#include <linux/pm_clock.h>
#include <linux/pm_runtime.h>
#include "ir.h"

#define DRIVER_NAME             "lombo-ir"

/* initialize IR register */
static void csp_ir_init(void *base)
{
	u32 src_freq, ir_clk_div, ir_work_freq, protocol;

	u32 frame_gap_ms_cnt;
	u32 frame_timeout_ms_cnt;
	u32 frame_timeout_reg_val;
	u32 bif_phase_us_cnt;
	u32 noise_th_reg_val;

	protocol = K_IR_PROTOCOL_NEC;

	/* protocol select */
	csp_ir_set_protocol(base, protocol);
	if (protocol == K_IR_PROTOCOL_SONY_SIRC)
		csp_ir_set_sirc_ext(base, IR_SIRC_AUTO);

	/* ir driver cfg */
	src_freq = K_IR_EFHOSC_FREQ;
	ir_work_freq = K_IR_WORK_CLK_FREQ;

	ir_clk_div = src_freq / ir_work_freq;
	csp_ir_set_clk_div(base, ir_clk_div);

	/* ir timeout threshold */
	if (protocol == K_IR_PROTOCOL_NEC)
		frame_gap_ms_cnt = K_IR_FRAME_GAP_NEC_MS_CNT;
	else if (protocol == K_IR_PROTOCOL_SONY_SIRC)
		frame_gap_ms_cnt = K_IR_FRAME_GAP_SIRC_MS_CNT;
	else if (protocol == K_IR_PROTOCOL_RC5)
		frame_gap_ms_cnt = K_IR_FRAME_GAP_RC5_MS_CNT;
	else if (protocol == K_IR_PROTOCOL_RC6)
		frame_gap_ms_cnt = K_IR_FRAME_GAP_RC6_MS_CNT;

	/* frame-timne-out = 2/3*frame-gap */
	frame_timeout_ms_cnt = frame_gap_ms_cnt * 2 / 3;

	/* (val+1) * 128 * 1/freq = timeout-ms-cnt/1000 */
	frame_timeout_reg_val = (frame_timeout_ms_cnt*ir_work_freq
		/ 1000 / 128) - 1;
	csp_ir_set_timeout_th(base, frame_timeout_reg_val);

	/* ir noise-threshold */
	if (protocol == K_IR_PROTOCOL_NEC)
		bif_phase_us_cnt = K_IR_BIT_PHASE_NEC_US_CNT;	/* 560us */
	else if (protocol == K_IR_PROTOCOL_SONY_SIRC)
		bif_phase_us_cnt = K_IR_BIT_PHASE_SIRC_US_CNT;	/* 600us */
	else if (protocol == K_IR_PROTOCOL_RC5)
		bif_phase_us_cnt = K_IR_BIT_PHASE_RC5_US_CNT;	/* 889us */
	else if (protocol == K_IR_PROTOCOL_RC6)
		bif_phase_us_cnt = K_IR_BIT_PHASE_RC6_US_CNT;	/* 444us */

	/* noise-th = 1/10 bit-phase */
	noise_th_reg_val = ((bif_phase_us_cnt * ir_work_freq
		/ 1000000 / 10) + 1);
	csp_ir_set_noise_th(base, noise_th_reg_val);

	/* set enable */
	csp_ir_set_en(base, true);
	csp_ir_set_int_en(base, K_IR_F_ERR, true);
	csp_ir_set_int_en(base, K_IR_REPEAT, true);
	csp_ir_set_int_en(base, K_IR_FRAME, true);
	csp_ir_set_int_en(base, K_IR_START, true);

#ifndef CONFIG_ARCH_LOMBO_N7V1
	/* default adjustment */
	csp_ir_set_short_adj(base, 0x03);
	csp_ir_set_long_adj(base, 0x03);
#endif
}

/* IR interrupt handle function */
static irqreturn_t lombo_ir_irq(int irq, void *data)
{
	struct lombo_ir_dev *ir_dev;
	u8 address, not_address;
	u8 command, not_command;
	u32 rawcode, scancode;
	enum rc_proto rc_proto;
	reg_ir_int_pd_t reg;

	ir_dev = data;
	reg.val = READREG32(ir_dev->base + IR_INT_PD_OFF);

	/* frame error interrupt */
	if (reg.bits.ir_f_err) {
		pr_info("<IR_INT_F_ERR>\n");
		csp_ir_int_clr(ir_dev->base, K_IR_F_ERR);
	}

	/* repeat interrupt */
	if (reg.bits.ir_repeat) {
		pr_info("<IR_INT_REPEAT>\n");
		csp_ir_int_clr(ir_dev->base, K_IR_REPEAT);

		rc_repeat(ir_dev->rcd);
	}

	/* start interrupt */
	if (reg.bits.ir_start) {
		pr_info("<IR_INT_START>\n");
		csp_ir_int_clr(ir_dev->base, K_IR_START);
	}

	/* frame interrupt */
	if (reg.bits.ir_frame) {
		pr_info("<IR_INT_FRAME>\n");
		csp_ir_int_clr(ir_dev->base, K_IR_FRAME);

		/* rc device code handle */
		rawcode = csp_ir_get_data(ir_dev->base);
		not_command = (rawcode >> 24) & 0xff;
		command = (rawcode >> 16) & 0xff;
		not_address = (rawcode >> 8) & 0xff;
		address = rawcode & 0xff;

		scancode = ir_nec_bytes_to_scancode(address, not_address,
						    command, not_command,
						    &rc_proto);
		pr_info("scancode = 0x%x\n", scancode);
		rc_keydown(ir_dev->rcd, rc_proto, scancode, 0);
	}

	return IRQ_HANDLED;
}

static int lombo_ir_probe(struct platform_device *pdev)
{
	struct device_node *np = pdev->dev.of_node;
	int ret;
	struct lombo_ir_dev *ir_dev;
	struct resource *res;
	struct rc_dev *rcd;

	pr_info("[IR] ===== lombo ir probe =====\n");

	ir_dev = devm_kzalloc(&pdev->dev,
		sizeof(struct lombo_ir_dev), GFP_KERNEL);
	if (!ir_dev) {
		PRT_ERR("failed to malloc memory\n");
		return -ENOMEM;
	}

	/* io resource */
	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	ir_dev->base = devm_ioremap_resource(&pdev->dev, res);
	if (IS_ERR(ir_dev->base)) {
		dev_err(&pdev->dev, "err ioremap invalid\n");
		return -EINVAL;
	}

	/* get ir apb gate */
	ir_dev->clk_gate = of_clk_get(np, 0);
	if (IS_ERR(ir_dev->clk_gate)) {
		PRT_ERR("ir get apb gate clk failed\n");
		return -EINVAL;
	}

	/* get ir apb reset */
	ir_dev->clk_reset = of_clk_get(np, 1);
	if (IS_ERR(ir_dev->clk_reset)) {
		PRT_ERR("ir get apb reset clk failed\n");
		return -EINVAL;
	}

#ifndef CONFIG_ARCH_LOMBO_N9V3
	/* remove ir module clk from prcm in n9v3 */
	/* get ir module clk */
	ir_dev->ir_clk = of_clk_get(np, 2);
	if (IS_ERR(ir_dev->ir_clk)) {
		PRT_ERR("ir get clk failed\n");
		return -EINVAL;
	}

	/* get ir parent clk */
	ir_dev->parent = of_clk_get(np, 3);
	if (IS_ERR(ir_dev->parent)) {
		PRT_ERR("ir get parent clk failed\n");
		return -EINVAL;
	}

	/* set ir parent clk */
	ret = clk_set_parent(ir_dev->ir_clk, ir_dev->parent);
	if (ret) {
		PRT_ERR("ir set parent clk failed\n");
		return ret;
	}

#ifndef CONFIG_PM_GENERIC_DOMAINS
	/* enable ir clk */
	ret = clk_prepare_enable(ir_dev->ir_clk);
	if (ret) {
		PRT_ERR("enable ir clock failed\n");
		return ret;
	}
#else
	ret = pm_clk_add_clk(&pdev->dev, ir_dev->ir_clk);
	if (ret) {
		PRT_ERR("add pm clk error\n");
		return ret;
	}
#endif
#endif

#ifndef CONFIG_PM_GENERIC_DOMAINS
	/* enable ir apb gate */
	ret = clk_prepare_enable(ir_dev->clk_gate);
	if (ret) {
		PRT_ERR("ir enable apb gate clk failed\n");
		return -EINVAL;
 	}
#else
	ret = pm_clk_add_clk(&pdev->dev, ir_dev->clk_gate);
	if (ret) {
		PRT_ERR("add pm clk error\n");
		return ret;
	}

	pm_clk_resume(&pdev->dev);
#endif

	/* enable ir apb reset */
	ret = clk_prepare_enable(ir_dev->clk_reset);
	if (ret) {
		PRT_ERR("ir enable apb reset clk failed\n");
		return -EINVAL;
	}


	/* rc device */
	rcd = devm_rc_allocate_device(&pdev->dev, RC_DRIVER_SCANCODE);
	if (!rcd) {
		dev_err(&pdev->dev, "failed to allocate rc device\n");
		return -EINVAL;
	}

	ir_dev->rcd = rcd;

	rcd->priv = ir_dev;
	rcd->input_phys = DRIVER_NAME "/input0";
	rcd->map_name = RC_MAP_LOMBO;
	rcd->allowed_protocols = RC_PROTO_BIT_NEC | RC_PROTO_BIT_NECX;
	rcd->driver_name = DRIVER_NAME;
	rcd->device_name = DRIVER_NAME;

	ret = devm_rc_register_device(&pdev->dev, rcd);
	if (ret) {
		dev_err(&pdev->dev, "failed to register rc device\n");
		return -EINVAL;
	}

	/* get irq */
	ir_dev->irq = platform_get_irq(pdev, 0);
	if (ir_dev->irq < 0) {
		dev_err(&pdev->dev, "platform_get_irq error: missing IRQ\n");
		return -EINVAL;
	}

	ret = devm_request_irq(&pdev->dev, ir_dev->irq, lombo_ir_irq,
				IRQF_SHARED, "lombo-ir", ir_dev);
	if (ret) {
		dev_err(&pdev->dev, "devm_request_irq error\n");
		return -EINVAL;
	}

	/* init ir register */
	csp_ir_init(ir_dev->base);

	platform_set_drvdata(pdev, ir_dev);

#ifdef CONFIG_PM_GENERIC_DOMAINS
	pm_runtime_set_active(&pdev->dev);
	/* enable the pm_runtime */
	pm_runtime_enable(&pdev->dev);
	pm_runtime_get_noresume(&pdev->dev);
#endif

	pr_info("[IR] ===== lombo ir probe finished =====\n");
	return 0;
}

static int lombo_ir_remove(struct platform_device *pdev)
{
	struct lombo_ir_dev *ir_dev;

	ir_dev = platform_get_drvdata(pdev);

	csp_ir_set_en(ir_dev->base, false);

#ifdef CONFIG_PM_GENERIC_DOMAINS
	pm_runtime_put_sync(&pdev->dev);
	pm_runtime_disable(&pdev->dev);
	pm_clk_remove_clk(&pdev->dev, ir_dev->clk_gate);
	pm_clk_remove_clk(&pdev->dev, ir_dev->ir_clk);
#else
	clk_disable_unprepare(ir_dev->clk_gate);
	clk_disable_unprepare(ir_dev->ir_clk);
	clk_put(ir_dev->clk_gate);
	clk_put(ir_dev->ir_clk);
#endif
	/* disable ir ahb reset clk */
	clk_disable_unprepare(ir_dev->clk_reset);
	/* put ir ahb reset clk */
	clk_put(ir_dev->clk_reset);

	return 0;
}

#ifdef CONFIG_PM_SLEEP
static int lombo_rc_suspend(struct device *dev)
{
#ifndef CONFIG_PM_GENERIC_DOMAINS
	struct lombo_ir_dev *ir_dev;
	struct platform_device *pdev = to_platform_device(dev);

	ir_dev = platform_get_drvdata(pdev);

	clk_disable_unprepare(ir_dev->clk_gate);
	clk_disable_unprepare(ir_dev->ir_clk);
#endif
	return 0;
}

static int lombo_rc_resume(struct device *dev)
{
	struct lombo_ir_dev *ir_dev;
	struct platform_device *pdev = to_platform_device(dev);

	ir_dev = platform_get_drvdata(pdev);

	clk_disable_unprepare(ir_dev->clk_reset);
	clk_prepare_enable(ir_dev->clk_reset);

#ifndef CONFIG_PM_GENERIC_DOMAINS
	clk_prepare_enable(ir_dev->ir_clk);
	clk_prepare_enable(ir_dev->clk_gate);
	csp_ir_init(ir_dev->base);
#else
	if (pm_runtime_status_suspended(dev))
		pm_clk_resume(dev);

	csp_ir_init(ir_dev->base);

	if (pm_runtime_status_suspended(dev))
		pm_clk_suspend(dev);
#endif

	return 0;
}
#endif

#ifdef CONFIG_PM
static const struct dev_pm_ops lombo_rc_pm_ops = {
#ifdef CONFIG_PM_SLEEP
	SET_SYSTEM_SLEEP_PM_OPS(lombo_rc_suspend,
		lombo_rc_resume)
#endif
};
#endif

#ifdef CONFIG_OF
static const struct of_device_id lombo_ir_dt_match[] = {
	{ .compatible = COMPATIBLE },
	{},
};
MODULE_DEVICE_TABLE(of, lombo_ir_dt_match);
#endif

static struct platform_driver lombo_ir_driver = {
	.probe		= lombo_ir_probe,
	.remove		= lombo_ir_remove,
	.driver		= {
		.name	= DRIVER_NAME,
		.owner	= THIS_MODULE,
	#ifdef CONFIG_OF
		.of_match_table = of_match_ptr(lombo_ir_dt_match),
	#endif
#ifdef CONFIG_PM
		.pm = &lombo_rc_pm_ops,
#endif
	},
};
module_platform_driver(lombo_ir_driver);

MODULE_DESCRIPTION("LomboTech ir driver");
MODULE_AUTHOR("lomboswer@lombotech.com");
MODULE_AUTHOR("lomboswer@lombotech.com");
MODULE_LICENSE("GPL");

